package simpledb.storage;

import simpledb.common.Database;
import simpledb.common.DbException;
import simpledb.common.Permissions;
import simpledb.transaction.TransactionAbortedException;
import simpledb.transaction.TransactionId;
import simpledb.util.HeapFileIterator;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

/**
 * HeapFile 是 SimpleDB 中实现的一个无序存储元组的数据库文件。
 * 它将数据存储在磁盘上，并通过 HeapPage 管理页面。每个页面是一个固定大小的块，而整个文件就是这些页面的集合。
 * HeapFile 与 HeapPage 密切合作，提供了对元组的基本操作，如插入、删除、读取等。
 * 它是 SimpleDB 存储层的重要组成部分，用于支持查询执行和其他高级功能。
 * HeapFile 是一种 DbFile 实现，它以无序的方式存储一组元组。
 */
public class HeapFile implements DbFile {
    // 文件对象，代表磁盘上的物理文件
    private final File       file;

    // 表的元组描述符
    private final TupleDesc  td;

    // 用于随机访问文件的对象
    private RandomAccessFile randomAccessFile;

    /**
     * 构造一个由指定文件支持的堆文件。
     *
     * @param f 用于存储堆文件的磁盘后端文件。
     * @param td 表的元组描述符。
     */
    public HeapFile(File f, TupleDesc td) {
        this.file = f;
        this.td = td;
        try {
            this.randomAccessFile = new RandomAccessFile(this.file, "rw"); // 初始化随机访问文件对象
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 返回此 HeapFile 在磁盘上的文件。
     *
     * @return 此 HeapFile 在磁盘上的文件。
     */
    public File getFile() {
        return this.file;
    }

    /**
     * 根据 PageId 读取页面。
     *
     * @param pid 页面的 ID。
     * @return 读取到的页面。
     */
    @Override
    public Page readPage(PageId pid) {
        final int pos = BufferPool.getPageSize() * pid.getPageNumber(); // 计算页面在文件中的位置
        byte[] pageData = new byte[BufferPool.getPageSize()];
        try {
            this.randomAccessFile.seek(pos); // 定位到页面起始位置
            this.randomAccessFile.read(pageData, 0, pageData.length); // 读取页面数据
            final HeapPage heapPage = new HeapPage((HeapPageId) pid, pageData); // 创建 HeapPage 对象
            return heapPage;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将页面写入磁盘。
     *
     * @param page 要写入的页面。
     * @throws IOException 如果写入过程中发生错误。
     */
    @Override
    public void writePage(Page page) throws IOException {
        final int pos = BufferPool.getPageSize() * page.getId().getPageNumber(); // 计算页面在文件中的位置
        this.randomAccessFile.seek(pos); // 定位到页面起始位置
        final byte[] pageData = page.getPageData(); // 获取页面数据
        this.randomAccessFile.write(pageData); // 写入页面数据
    }

    /**
     * 插入一个新的元组。
     *
     * @param tid 当前事务的 ID。
     * @param t 要插入的元组。
     * @return 包含所有被修改的页面的列表。
     * @throws DbException 如果插入过程中发生数据库错误。
     * @throws IOException 如果写入过程中发生 I/O 错误。
     * @throws TransactionAbortedException 如果事务被中止。
     */
    @Override
    public List<Page> insertTuple(TransactionId tid, Tuple t) throws DbException, IOException,
                                                             TransactionAbortedException {
        final ArrayList<Page> dirtyPageList = new ArrayList<>();
        for (int i = 0; i < this.numPages(); i++) {
            final HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, new HeapPageId(getId(), i),
                Permissions.READ_WRITE);
            if (page != null && page.getNumEmptySlots() > 0) { // 检查是否有空槽位
                page.insertTuple(t); // 插入元组
                page.markDirty(true, tid); // 标记页面为脏页
                dirtyPageList.add(page); // 添加到脏页列表
                break;
            }
        }
        // 如果所有页面都已满，则创建新页面
        if (dirtyPageList.size() == 0) {
            final HeapPageId heapPageId = new HeapPageId(getId(), this.numPages());
            HeapPage newPage = new HeapPage(heapPageId, HeapPage.createEmptyPageData()); // 创建新的空页面
            writePage(newPage); // 将新页面写入磁盘
            newPage = (HeapPage) Database.getBufferPool().getPage(tid, heapPageId, Permissions.READ_WRITE); // 从缓冲池获取新页面
            newPage.insertTuple(t); // 插入元组
            newPage.markDirty(true, tid); // 标记页面为脏页
            dirtyPageList.add(newPage); // 添加到脏页列表
        }
        return dirtyPageList;
    }

    /**
     * 删除指定的元组。
     *
     * @param tid 当前事务的 ID。
     * @param t 要删除的元组。
     * @return 包含所有被修改的页面的列表。
     * @throws DbException 如果删除过程中发生数据库错误。
     * @throws TransactionAbortedException 如果事务被中止。
     */
    @Override
    public List<Page> deleteTuple(TransactionId tid, Tuple t) throws DbException, IOException,
                                                             TransactionAbortedException {
        final ArrayList<Page> dirtyPageList = new ArrayList<>();
        final RecordId recordId = t.getRecordId();
        final PageId pageId = recordId.getPageId();
        final HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, pageId, Permissions.READ_WRITE);
        if (page != null && page.isSlotUsed(recordId.getTupleNumber())) { // 检查槽位是否使用
            page.deleteTuple(t); // 删除元组
            dirtyPageList.add(page); // 添加到脏页列表
        }
        return dirtyPageList;
    }

    /**
     * 返回一个迭代器，用于遍历表中的所有元组。
     *
     * @param tid 当前事务的 ID。
     * @return 迭代器对象。
     */
    @Override
    public DbFileIterator iterator(TransactionId tid) {
        return new HeapFileIterator(numPages(), tid, this.getId()); // 创建并返回 HeapFileIterator 对象
    }

    /**
     * 返回唯一标识此 HeapFile 的 ID。
     * 实现注意：你需要生成这个 tableid 来确保每个 HeapFile 都有一个“唯一ID”，并且总是返回相同的值。
     * 建议使用文件的绝对路径进行哈希，例如 f.getAbsoluteFile().hashCode()。
     *
     * @return 唯一标识此 HeapFile 的 ID。
     */
    @Override
    public int getId() {
        return this.file.getAbsolutePath().hashCode();
    }

    /**
     * 返回存储在此 DbFile 中的表的 TupleDesc。
     *
     * @return 此 DbFile 中表的 TupleDesc。
     */
    @Override
    public TupleDesc getTupleDesc() {
        return this.td;
    }

    /**
     * 返回此 HeapFile 中的页面数量。
     *
     * @return 此 HeapFile 中的页面数量。
     */
    public int numPages() {
        return (int) this.file.length() / BufferPool.getPageSize(); // 计算文件长度除以页面大小得到页面数
    }
}
